//
// MonoSymbolFile.cs
//
// Authors:
//   Martin Baulig (martin@ximian.com)
//   Marek Safar (marek.safar@gmail.com)
//
// (C) 2003 Ximian, Inc.  http://www.ximian.com
// Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com)
//
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;

namespace ILRuntime.Mono.CompilerServices.SymbolWriter
{
    public class MonoSymbolFileException : Exception
    {
        public MonoSymbolFileException()
            : base()
        { }

        public MonoSymbolFileException(string message, params object[] args)
            : base(String.Format(message, args))
        {
        }

        public MonoSymbolFileException(string message, Exception innerException)
            : base(message, innerException)
        {
        }
    }

    sealed class MyBinaryWriter : BinaryWriter
    {
        public MyBinaryWriter(Stream stream)
            : base(stream)
        { }

        public void WriteLeb128(int value)
        {
            base.Write7BitEncodedInt(value);
        }
    }

    internal class MyBinaryReader : BinaryReader
    {
        public MyBinaryReader(Stream stream)
            : base(stream)
        { }

        public int ReadLeb128()
        {
            return base.Read7BitEncodedInt();
        }

        public string ReadString(int offset)
        {
            long old_pos = BaseStream.Position;
            BaseStream.Position = offset;

            string text = ReadString();

            BaseStream.Position = old_pos;
            return text;
        }
    }

    public interface ISourceFile
    {
        SourceFileEntry Entry
        {
            get;
        }
    }

    public interface ICompileUnit
    {
        CompileUnitEntry Entry
        {
            get;
        }
    }

    public interface IMethodDef
    {
        string Name
        {
            get;
        }

        int Token
        {
            get;
        }
    }

    public class MonoSymbolFile : IDisposable
    {
        List<MethodEntry> methods = new List<MethodEntry>();
        List<SourceFileEntry> sources = new List<SourceFileEntry>();
        List<CompileUnitEntry> comp_units = new List<CompileUnitEntry>();
        Dictionary<int, AnonymousScopeEntry> anonymous_scopes;

        OffsetTable ot;
        int last_type_index;
        int last_method_index;
        int last_namespace_index;

        public readonly int MajorVersion = OffsetTable.MajorVersion;
        public readonly int MinorVersion = OffsetTable.MinorVersion;

        public int NumLineNumbers;

        public MonoSymbolFile()
        {
            ot = new OffsetTable();
        }

        public int AddSource(SourceFileEntry source)
        {
            sources.Add(source);
            return sources.Count;
        }

        public int AddCompileUnit(CompileUnitEntry entry)
        {
            comp_units.Add(entry);
            return comp_units.Count;
        }

        public void AddMethod(MethodEntry entry)
        {
            methods.Add(entry);
        }

        public MethodEntry DefineMethod(CompileUnitEntry comp_unit, int token,
                         ScopeVariable[] scope_vars, LocalVariableEntry[] locals,
                         LineNumberEntry[] lines, CodeBlockEntry[] code_blocks,
                         string real_name, MethodEntry.Flags flags,
                         int namespace_id)
        {
            if (reader != null)
                throw new InvalidOperationException();

            MethodEntry method = new MethodEntry(
                this, comp_unit, token, scope_vars, locals, lines, code_blocks,
                real_name, flags, namespace_id);
            AddMethod(method);
            return method;
        }

        internal void DefineAnonymousScope(int id)
        {
            if (reader != null)
                throw new InvalidOperationException();

            if (anonymous_scopes == null)
                anonymous_scopes = new Dictionary<int, AnonymousScopeEntry>();

            anonymous_scopes.Add(id, new AnonymousScopeEntry(id));
        }

        internal void DefineCapturedVariable(int scope_id, string name, string captured_name,
                              CapturedVariable.CapturedKind kind)
        {
            if (reader != null)
                throw new InvalidOperationException();

            AnonymousScopeEntry scope = anonymous_scopes[scope_id];
            scope.AddCapturedVariable(name, captured_name, kind);
        }

        internal void DefineCapturedScope(int scope_id, int id, string captured_name)
        {
            if (reader != null)
                throw new InvalidOperationException();

            AnonymousScopeEntry scope = anonymous_scopes[scope_id];
            scope.AddCapturedScope(id, captured_name);
        }

        internal int GetNextTypeIndex()
        {
            return ++last_type_index;
        }

        internal int GetNextMethodIndex()
        {
            return ++last_method_index;
        }

        internal int GetNextNamespaceIndex()
        {
            return ++last_namespace_index;
        }

        void Write(MyBinaryWriter bw, Guid guid)
        {
            // Magic number and file version.
            bw.Write(OffsetTable.Magic);
            bw.Write(MajorVersion);
            bw.Write(MinorVersion);

            bw.Write(guid.ToByteArray());

            //
            // Offsets of file sections; we must write this after we're done
            // writing the whole file, so we just reserve the space for it here.
            //
            long offset_table_offset = bw.BaseStream.Position;
            ot.Write(bw, MajorVersion, MinorVersion);

            //
            // Sort the methods according to their tokens and update their index.
            //
            methods.Sort();
            for (int i = 0; i < methods.Count; i++)
                methods[i].Index = i + 1;

            //
            // Write data sections.
            //
            ot.DataSectionOffset = (int)bw.BaseStream.Position;
            foreach (SourceFileEntry source in sources)
                source.WriteData(bw);
            foreach (CompileUnitEntry comp_unit in comp_units)
                comp_unit.WriteData(bw);
            foreach (MethodEntry method in methods)
                method.WriteData(this, bw);
            ot.DataSectionSize = (int)bw.BaseStream.Position - ot.DataSectionOffset;

            //
            // Write the method index table.
            //
            ot.MethodTableOffset = (int)bw.BaseStream.Position;
            for (int i = 0; i < methods.Count; i++)
            {
                MethodEntry entry = methods[i];
                entry.Write(bw);
            }
            ot.MethodTableSize = (int)bw.BaseStream.Position - ot.MethodTableOffset;

            //
            // Write source table.
            //
            ot.SourceTableOffset = (int)bw.BaseStream.Position;
            for (int i = 0; i < sources.Count; i++)
            {
                SourceFileEntry source = sources[i];
                source.Write(bw);
            }
            ot.SourceTableSize = (int)bw.BaseStream.Position - ot.SourceTableOffset;

            //
            // Write compilation unit table.
            //
            ot.CompileUnitTableOffset = (int)bw.BaseStream.Position;
            for (int i = 0; i < comp_units.Count; i++)
            {
                CompileUnitEntry unit = comp_units[i];
                unit.Write(bw);
            }
            ot.CompileUnitTableSize = (int)bw.BaseStream.Position - ot.CompileUnitTableOffset;

            //
            // Write anonymous scope table.
            //
            ot.AnonymousScopeCount = anonymous_scopes != null ? anonymous_scopes.Count : 0;
            ot.AnonymousScopeTableOffset = (int)bw.BaseStream.Position;
            if (anonymous_scopes != null)
            {
                foreach (AnonymousScopeEntry scope in anonymous_scopes.Values)
                    scope.Write(bw);
            }
            ot.AnonymousScopeTableSize = (int)bw.BaseStream.Position - ot.AnonymousScopeTableOffset;

            //
            // Fixup offset table.
            //
            ot.TypeCount = last_type_index;
            ot.MethodCount = methods.Count;
            ot.SourceCount = sources.Count;
            ot.CompileUnitCount = comp_units.Count;

            //
            // Write offset table.
            //
            ot.TotalFileSize = (int)bw.BaseStream.Position;
            bw.Seek((int)offset_table_offset, SeekOrigin.Begin);
            ot.Write(bw, MajorVersion, MinorVersion);
            bw.Seek(0, SeekOrigin.End);

#if false
			Console.WriteLine ("TOTAL: {0} line numbes, {1} bytes, extended {2} bytes, " +
					   "{3} methods.", NumLineNumbers, LineNumberSize,
					   ExtendedLineNumberSize, methods.Count);
#endif
        }

        public void CreateSymbolFile(Guid guid, FileStream fs)
        {
            if (reader != null)
                throw new InvalidOperationException();

            Write(new MyBinaryWriter(fs), guid);
        }

        MyBinaryReader reader;
        Dictionary<int, SourceFileEntry> source_file_hash;
        Dictionary<int, CompileUnitEntry> compile_unit_hash;

        List<MethodEntry> method_list;
        Dictionary<int, MethodEntry> method_token_hash;
        Dictionary<string, int> source_name_hash;

        Guid guid;

        MonoSymbolFile(Stream stream)
        {
            reader = new MyBinaryReader(stream);

            try
            {
                long magic = reader.ReadInt64();
                int major_version = reader.ReadInt32();
                int minor_version = reader.ReadInt32();

                if (magic != OffsetTable.Magic)
                    throw new MonoSymbolFileException("Symbol file is not a valid");
                if (major_version != OffsetTable.MajorVersion)
                    throw new MonoSymbolFileException(
                        "Symbol file has version {0} but expected {1}", major_version, OffsetTable.MajorVersion);
                if (minor_version != OffsetTable.MinorVersion)
                    throw new MonoSymbolFileException("Symbol file has version {0}.{1} but expected {2}.{3}",
                        major_version, minor_version,
                        OffsetTable.MajorVersion, OffsetTable.MinorVersion);

                MajorVersion = major_version;
                MinorVersion = minor_version;
                guid = new Guid(reader.ReadBytes(16));

                ot = new OffsetTable(reader, major_version, minor_version);
            }
            catch (Exception e)
            {
                throw new MonoSymbolFileException("Cannot read symbol file", e);
            }

            source_file_hash = new Dictionary<int, SourceFileEntry>();
            compile_unit_hash = new Dictionary<int, CompileUnitEntry>();
        }

#if !NET_CORE
        public static MonoSymbolFile ReadSymbolFile(Assembly assembly)
        {
            string filename = assembly.Location;
            string name = filename + ".mdb";

            Module[] modules = assembly.GetModules();
            Guid assembly_guid = modules[0].ModuleVersionId;

            return ReadSymbolFile(name, assembly_guid);
        }
#endif

        public static MonoSymbolFile ReadSymbolFile(string mdbFilename)
        {
            return ReadSymbolFile(new FileStream(mdbFilename, FileMode.Open, FileAccess.Read));
        }

        public static MonoSymbolFile ReadSymbolFile(string mdbFilename, Guid assemblyGuid)
        {
            MonoSymbolFile sf = ReadSymbolFile(mdbFilename);
            if (assemblyGuid != sf.guid)
                throw new MonoSymbolFileException("Symbol file `{0}' does not match assembly", mdbFilename);

            return sf;
        }

        public static MonoSymbolFile ReadSymbolFile(Stream stream)
        {
            return new MonoSymbolFile(stream);
        }

        public int CompileUnitCount
        {
            get { return ot.CompileUnitCount; }
        }

        public int SourceCount
        {
            get { return ot.SourceCount; }
        }

        public int MethodCount
        {
            get { return ot.MethodCount; }
        }

        public int TypeCount
        {
            get { return ot.TypeCount; }
        }

        public int AnonymousScopeCount
        {
            get { return ot.AnonymousScopeCount; }
        }

        public int NamespaceCount
        {
            get { return last_namespace_index; }
        }

        public Guid Guid
        {
            get { return guid; }
        }

        public OffsetTable OffsetTable
        {
            get { return ot; }
        }

        internal int LineNumberCount = 0;
        internal int LocalCount = 0;
        internal int StringSize = 0;

        internal int LineNumberSize = 0;
        internal int ExtendedLineNumberSize = 0;

        public SourceFileEntry GetSourceFile(int index)
        {
            if ((index < 1) || (index > ot.SourceCount))
                throw new ArgumentException();
            if (reader == null)
                throw new InvalidOperationException();

            lock (this)
            {
                if (source_file_hash.TryGetValue(index, out SourceFileEntry source))
                    return source;

                long old_pos = reader.BaseStream.Position;

                reader.BaseStream.Position = ot.SourceTableOffset +
                    SourceFileEntry.Size * (index - 1);
                source = new SourceFileEntry(this, reader);
                source_file_hash.Add(index, source);

                reader.BaseStream.Position = old_pos;
                return source;
            }
        }

        public SourceFileEntry[] Sources
        {
            get
            {
                if (reader == null)
                    throw new InvalidOperationException();

                SourceFileEntry[] retval = new SourceFileEntry[SourceCount];
                for (int i = 0; i < SourceCount; i++)
                    retval[i] = GetSourceFile(i + 1);
                return retval;
            }
        }

        public CompileUnitEntry GetCompileUnit(int index)
        {
            if ((index < 1) || (index > ot.CompileUnitCount))
                throw new ArgumentException();
            if (reader == null)
                throw new InvalidOperationException();

            lock (this)
            {
                if (compile_unit_hash.TryGetValue(index, out CompileUnitEntry unit))
                    return unit;

                long old_pos = reader.BaseStream.Position;

                reader.BaseStream.Position = ot.CompileUnitTableOffset +
                    CompileUnitEntry.Size * (index - 1);
                unit = new CompileUnitEntry(this, reader);
                compile_unit_hash.Add(index, unit);

                reader.BaseStream.Position = old_pos;
                return unit;
            }
        }

        public CompileUnitEntry[] CompileUnits
        {
            get
            {
                if (reader == null)
                    throw new InvalidOperationException();

                CompileUnitEntry[] retval = new CompileUnitEntry[CompileUnitCount];
                for (int i = 0; i < CompileUnitCount; i++)
                    retval[i] = GetCompileUnit(i + 1);
                return retval;
            }
        }

        void read_methods()
        {
            lock (this)
            {
                if (method_token_hash != null)
                    return;

                method_token_hash = new Dictionary<int, MethodEntry>();
                method_list = new List<MethodEntry>();

                long old_pos = reader.BaseStream.Position;
                reader.BaseStream.Position = ot.MethodTableOffset;

                for (int i = 0; i < MethodCount; i++)
                {
                    MethodEntry entry = new MethodEntry(this, reader, i + 1);
                    method_token_hash.Add(entry.Token, entry);
                    method_list.Add(entry);
                }

                reader.BaseStream.Position = old_pos;
            }
        }

        public MethodEntry GetMethodByToken(int token)
        {
            if (reader == null)
                throw new InvalidOperationException();

            lock (this)
            {
                read_methods();
                method_token_hash.TryGetValue(token, out MethodEntry me);
                return me;
            }
        }

        public MethodEntry GetMethod(int index)
        {
            if ((index < 1) || (index > ot.MethodCount))
                throw new ArgumentException();
            if (reader == null)
                throw new InvalidOperationException();

            lock (this)
            {
                read_methods();
                return method_list[index - 1];
            }
        }

        public MethodEntry[] Methods
        {
            get
            {
                if (reader == null)
                    throw new InvalidOperationException();

                lock (this)
                {
                    read_methods();
                    MethodEntry[] retval = new MethodEntry[MethodCount];
                    method_list.CopyTo(retval, 0);
                    return retval;
                }
            }
        }

        public int FindSource(string file_name)
        {
            if (reader == null)
                throw new InvalidOperationException();

            lock (this)
            {
                if (source_name_hash == null)
                {
                    source_name_hash = new Dictionary<string, int>();

                    for (int i = 0; i < ot.SourceCount; i++)
                    {
                        SourceFileEntry source = GetSourceFile(i + 1);
                        source_name_hash.Add(source.FileName, i);
                    }
                }

                if (!source_name_hash.TryGetValue(file_name, out int value))
                    return -1;
                return value;
            }
        }

        public AnonymousScopeEntry GetAnonymousScope(int id)
        {
            if (reader == null)
                throw new InvalidOperationException();

            AnonymousScopeEntry scope;
            lock (this)
            {
                if (anonymous_scopes != null)
                {
                    anonymous_scopes.TryGetValue(id, out scope);
                    return scope;
                }

                anonymous_scopes = new Dictionary<int, AnonymousScopeEntry>();
                reader.BaseStream.Position = ot.AnonymousScopeTableOffset;
                for (int i = 0; i < ot.AnonymousScopeCount; i++)
                {
                    scope = new AnonymousScopeEntry(reader);
                    anonymous_scopes.Add(scope.ID, scope);
                }

                return anonymous_scopes[id];
            }
        }

        internal MyBinaryReader BinaryReader
        {
            get
            {
                if (reader == null)
                    throw new InvalidOperationException();

                return reader;
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (reader != null)
                {
#if NET_CORE
					reader.Dispose ();
#else
                    reader.Close();
#endif
                    reader = null;
                }
            }
        }
    }
}
